//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#ifndef _BUOLA_IO_CINDIRECTBUFFERSTREAM_H_
#define _BUOLA_IO_CINDIRECTBUFFERSTREAM_H_

#include <buola/io/cbufferstream.h>
#include <buola/io/cstream.h>

namespace buola { namespace io {

class CIndirectBufferStream : public CBufferStream
{
    static const size_t cDefaultBufferLength=8192;
public:
    CIndirectBufferStream(PStream pStream)
        :   mStream(std::move(pStream))
    {
        mBegin=alloc_array<uint8_t>(cDefaultBufferLength);
        mEnd=mBegin+cDefaultBufferLength;
        mWPtr=mBegin;
        mWEnd=mEnd;
        mRPtr=mREnd=mBegin;
    }

public:
    virtual ~CIndirectBufferStream()
    {
        DoFlush();
        free(mBegin);
    }
    
private:
    CResult DoRead(uint8_t *pData,size_t pLen) final
    {
        ssize_t lRead=0;
        
        if(mWEnd!=mBegin) //there's a write buffer... flush it
        {
            if(mWPtr>mBegin)
                DoFlush();
            mWEnd=mBegin;
        }
        else if(mRPtr<mREnd)
        {
            //copy what's available... it's guaranteed that not everything is available
            lRead=mREnd-mRPtr;
            memcpy(pData,mRPtr,lRead);
            pData+=lRead;
            pLen-=lRead;
        }
        
        mREnd=mRPtr=mBegin;

        if(pLen>(mEnd-mBegin)/2)
        {
            //just do a direct read into buffer
            CResult lRes=mStream->Read(pData,pLen);
            return CResult(lRead+lRes.Count(),lRes.Flags());
        }
        
        io::CResult lRes=mStream->Read(mBegin,mEnd-mBegin);
        if(lRes.Count()>=pLen)
        {
            memcpy(pData,mBegin,pLen);
            if(lRes.Count()>pLen)
            {
                mREnd=mBegin+lRes.Count();
                mRPtr=mBegin+pLen;
            }
            else
            {
                mREnd=mBegin;
                mRPtr=mBegin;
            }
            return CResult(lRead+pLen);
        }
        else
        {
            if(lRes.Count())
                memcpy(pData,mBegin,lRes.Count());
            mREnd=mBegin;
            mRPtr=mBegin;
            return CResult(lRead+lRes.Count(),lRes.Flags());
        }
    }
    
    CResult DoIgnore(size_t pLen) final
    {
        ssize_t lIgnored=0;
        
        if(mWEnd!=mBegin) //there's a write buffer... flush it
        {
            if(mWPtr>mBegin)
                DoFlush();
            mWEnd=mBegin;
        }
        else if(mRPtr<mREnd)
        {
            //ignore what's available... it's guaranteed that not everything is available
            lIgnored=mREnd-mRPtr;
            pLen-=lIgnored;
        }
        
        mREnd=mRPtr=mBegin;

        ///\todo check that stream is seekable and that you can seek that far
        DoSeek(pLen,ESeek::RELATIVE);
        return CResult(lIgnored+pLen,EResult::NONE);
    }
    
    CResult DoWrite(const uint8_t *pData,size_t pLen) final
    {
        ssize_t lWritten=0;
        
        if(mWEnd==mBegin) //no write buffer
        {
            ///\todo rewind the header... this messes it up
            mREnd=mBegin;
            mRPtr=mBegin;
            mWEnd=mEnd;
        }
        else
        {
            if(mWPtr<mWEnd)
            {
                lWritten=mWEnd-mWPtr;
                memcpy(mWPtr,pData,lWritten);
                mWPtr=mWEnd;
                pData+=lWritten;
                pLen-=lWritten;
            }
            
            DoFlush();
        }
        
        if(pLen>(mEnd-mBegin)/2)
        {
            //just do a direct write from buffer
            CResult lRes=mStream->Write(pData,pLen);
            ///\todo buffer what can't be written... no limit?
            return CResult(lWritten+lRes.Count(),lRes.Flags());
        }
        
        memcpy(mBegin,pData,pLen);
        mWPtr=mBegin+pLen;
        return CResult(lWritten+pLen);
    }
    
    CResult DoUnderflow() final
    {
        //I can assume there's nothing in read buffer
        if(mWEnd!=mBegin) //there's a write buffer
        {
            if(mWPtr>mBegin)
                DoFlush();
            mWEnd=mBegin;
        }
        
        mREnd=mRPtr=mBegin;

        CResult lRes=mStream->Read(mBegin,mEnd-mBegin);
        if(lRes.Count()==0) return lRes;
        mREnd=mBegin+lRes.Count();
        return CResult(lRes.Count());
    }
    
    off_t DoSeek(off_t pPos,ESeek pMode) final
    {
        if(mWEnd!=mBegin) //we are in write mode
        {
            DoFlush();
            return mStream->Seek(pPos,pMode);
        }
        else
        {
            if(pMode==ESeek::RELATIVE)
            {
                if(pPos==0) //optimize this
                {
                    off_t lPos=mStream->Seek(0,ESeek::RELATIVE);
                    return lPos-(mREnd-mRPtr);
                }
                else
                {
                    off_t lPos=mStream->Seek(pPos-(mREnd-mRPtr),ESeek::RELATIVE);
                    mREnd=mRPtr=mBegin;
                    return lPos;
                }
            }
            else 
            {
                off_t lPos=mStream->Seek(pPos,pMode);
                mREnd=mRPtr=mBegin;
                return lPos;
            }
        }
    }
    
    void DoFlush() final
    {
        if(mWPtr>mBegin)
        {
            ///\todo check errors
            mStream->Write(mBegin,mWPtr-mBegin);
            mWPtr=mBegin;
            mWEnd=mEnd;
        }
    }
    
    off_t DoTell() final
    {
        //maybe it would be good to keep track of the current position to return this automatically
        return DoSeek(0,ESeek::RELATIVE);
    }
    
protected:
    uint8_t *mBegin;
    uint8_t *mEnd;
    PStream mStream;
};

/*namespace io*/ } /*namespace buola*/ }

#endif
